<template>
  <div class="generatePdfContainer">
    <div
      class="previewContainer"
      :style="{
        width: pdfConfig.rectOriginMM.width
      }"
    >
      <div
        v-if="headerClass"
        ref="header"
        class="headerContainer"
      >
        <slot name="header"></slot>
      </div>
      
      <div
        ref="default"
        class="defaultContainer scroll-bar"
        :class="containerClass"
        :style="{
          height: contentHeight
        }"
      >
        <slot></slot>
      </div>
      
      <div
        v-if="footerClass"
        ref="footer"
        class="footerContainer"
        :style="{
          paddingTop: (config?.bottomDistance ?? 0) + 'px'
        }"
      >
        <slot name="footer"></slot>
      </div>
    </div>
    
    <div
      class="maskLayer"
      :style="{
        height: maskVisible ? '100vh' : '0'
      }"
    >
      <i
        v-if="maskVisible"
        class="closeBtn el-icon-close"
        @click="() => maskVisibleControl(false)"
      />
      <div
        ref="generate"
        class="generateContainer scroll-bar"
      />
    </div>
  </div>
</template>

<script>
import html2canvas from 'html2canvas';
import JsPDF from 'jspdf';
import {
  A4OriginFormat,
  blobUriToFile,
  Dpi,
  isBrower
} from './utils';
export default {
  name: 'GeneratePdf',
  props: {
    /** 页眉处 页码放置位置对应的类名，当有此值时，即代表有页眉dom，并且页码放在这个类名下 */
    headerClass: {
      type: String,
      default: ''
    },
    /** 页脚处 页码放置位置对应的类名，当有此值时，即代表有页脚dom，并且页码放在这个类名下 */
    footerClass: {
      type: String,
      default: ''
    },
    /** 内容区对应的类名，可用来统一布局内容区页面样式 */
    containerClass: {
      type: String,
      default: ''
    },
    /** 页面配置文件 => 输出PDF类型及方向 */
    config: {
      type: Object,
      default: () => {
        return {
          type: 'A4',
          direction: 'Vertical',
          bottomDistance: 0,
          pageBreakClass: '',
          mode: 'auto',
          autoDownload: true
        }
      }
    }
  },
  data() {
    return {
      defaultConfig: {
        type: 'A4',
        direction: 'Vertical',
        bottomDistance: 0,
        pageBreakClass: '',
        mode: 'auto',
        autoDownload: true
      },
      // props config属性 传值和默认值合并处理
      mergedPropsConfig: {},
      PDF: null, // PDF对象
      getDiv: null, // html2canvas所需的dom
      pageConfig: {
        width: 0,
        height: 0
      },
      /** 当前模式：preview 预览模式 | generate 生成模式 */
      mode: '',
      maskVisible: false, // 预览PDF的蒙层遮罩
      /** 生成状态 => 用于生成pdf时 */
      generateStatus: {
        status: 'ready', // ready | process | finish
        process: 0, // 当处于process时，此属性生效，表示生成进度 0-100
        curPage: 0, // 当前生成的页数
        totalPage: 0 // pdf总页数
      },
      /** pdf生成时，内部可配置参数 */
      pdfInnerConfig: {
        scale: 3.125 // canvas的缩放倍数
      },
      contentHeight: '100%',
      /** 预览/生成分页时的计算数据 */
      pageBreakData: {
        curHeight: 0, // 当前页的高度
        totalHeight: 0, // 累计总高度
        borderSegmentHeight: [], // 默认模式使用 => 每个页面边框的高度
        pageNo: 0, // 页码数
        pageDiv: null // 预览/生成 pdf的dom容器
      },
      /** pdf纸张配置 */
      pdfConfig: {
        direction: 'Vertical',
        rectOriginMM: {
          width: '',
          numWidth: 0,
          height: '',
          numHeight: 0
        },
        rectOriginPX300DPI: {
          width: 0,
          height: 0
        }
      }
    }
  },
  watch: {
    config: {
      handler(val) {
        this.mergedPropsConfig = ({
          ...this.defaultConfig,
          ...val
        });
      },
      deep: true,
      immediate: true
    }
  },
  mounted() {
    this.getDpi(this.mergedPropsConfig.type, this.mergedPropsConfig.direction);
    this.calcPageConfig();
  },
  methods: {
    /**
     * 获取当前显示分辨率 一般都为96dpi, 以此来计算当前一页的高度
     */
    getDpi(type, direction = 'Vertical') {
      if (type === 'A4') {
        const { A4OriginMM, A4OriginPX300DPI } = A4OriginFormat(direction);
        this.pdfConfig.direction = direction;
        this.pdfConfig.rectOriginMM.width = A4OriginMM.width + 'mm';
        this.pdfConfig.rectOriginMM.numWidth = A4OriginMM.width;
        this.pdfConfig.rectOriginMM.height = A4OriginMM.height + 'mm';
        this.pdfConfig.rectOriginMM.numHeight = A4OriginMM.height;
        this.pdfConfig.rectOriginPX300DPI = A4OriginPX300DPI;
        window.pdfConfig = this.pdfConfig;
      }
    },
    calcPageConfig() {
      this.pageConfig.width = Number(new Dpi().mmToPx(this.pdfConfig.rectOriginMM.numWidth));
      this.pageConfig.height = Number(new Dpi().mmToPx(this.pdfConfig.rectOriginMM.numHeight));
    },
    /**
     * 重置生成状态
     */
    resetGenerateStatus() {
      this.generateStatus.status = 'ready';
      this.generateStatus.process = 0;
      this.generateStatus.curPage = 0;
      this.generateStatus.totalPage = 0;
      // 清除所有生成dom
      this.$refs.generate.replaceChildren();
      // 重置预览/生成 分页时的计算数据
      this.pageBreakData.curHeight = 0;
      this.pageBreakData.totalHeight = 0;
      this.pageBreakData.borderSegmentHeight = [];
      this.pageBreakData.pageNo = 0;
      this.pageBreakData.pageDiv = null;
    },
    /**
     * 分页逻辑 => 开始新的一页分页
     * @param fragment html片段
     */
    addPage(fragment) {
      this.pageBreakData.curHeight = 0;
      this.pageBreakData.pageNo++;
      this.pageBreakData.pageDiv = document.createElement('div');
      this.pageBreakData.pageDiv.classList.add('page-no', `page-no-${this.pageBreakData.pageNo}`);
      // 当containerClass有值再添加，否则dom报错
      if (this.containerClass) {
        this.pageBreakData.pageDiv.classList.add(this.containerClass);
      }
      this.pageBreakData.pageDiv.dataset.pageNo = this.pageBreakData.pageNo.toString();
      this.pageBreakData.pageDiv.style.width = `${this.pdfConfig.rectOriginMM.width}`;
      this.pageBreakData.pageDiv.style.height = `${this.pdfConfig.rectOriginMM.height}`;
      fragment.appendChild(this.pageBreakData.pageDiv);
      // 当页眉插槽等数据存在 并且非默认模式
      const isAddHeader = this.headerClass && this.$refs.header
      if (isAddHeader) {
        const cloneHeader = this._fragmentCloneNode(this.$refs.header);
        cloneHeader.style.width = '100%';
        cloneHeader.style.position = 'relative';
        this.pageBreakData.pageDiv.appendChild(cloneHeader);
      }
    },
    /**
     * 闭页逻辑 => 完成当前页的分页
     */
    finishPage() {
      // 当页脚插槽等数据存在 并且非默认模式
      const isAddFooter = this.footerClass && this.$refs.footer
      if (isAddFooter) {
        const cloneFooter = this._fragmentCloneNode(this.$refs.footer);
        // 填充当前页页码
        cloneFooter.querySelector(`.${this.footerClass}`).textContent = this.pageBreakData.pageNo;
        // 将生成页的页脚置于底部，不care用户自行设置的样式
        cloneFooter.style.position = 'absolute';
        cloneFooter.style.bottom = '0';
        cloneFooter.style.left = '0';
        cloneFooter.style.width = '100%';
        cloneFooter.style.backgroundColor = '#fff';
        this.pageBreakData.pageDiv.appendChild(cloneFooter);
      }
    },
    /**
     * 分页逻辑
     */
    splitAndGenerateDom() {
      // 每次预览/生成 pdf时都重新清除dom
      this.resetGenerateStatus();
      return new Promise((resolve) => {
        // 页面总数没有被重置为0 直接返回,不进行后续逻辑
        if (this.generateStatus.totalPage !== 0) resolve(false);
        // 没有传入dom进来 直接返回，不进行后续逻辑
        if (!this.$slots.default) resolve(false);
        let headerHieght = 0;
        let footerHieght = 0;
        if (this.headerClass && this.$refs.header) {
          headerHieght = this.$refs.header.offsetHeight;
        }
        if (this.footerClass && this.$refs.footer) {
          footerHieght = this.$refs.footer.offsetHeight;
        }
        const contentHeight = this.pageConfig.height - headerHieght - footerHieght;
        if (this.mergedPropsConfig.mode === 'auto') {
          this.pageBreakByAuto(contentHeight);
        } else {
          this.pageBreakByDefault(contentHeight);
        }
        resolve(true);
      })
    },
    /**
     * @description 分页时递归判断是否存在vue-frag的fragment节点
     */
    _fragmentWhileFun(node, dealFunc) {
      if (node.frag) {
        for (let i = 0; i < node.frag.length; i++) {
          const child = node.frag[i];
          if (child.frag) {
            this._fragmentWhileFun(child, dealFunc)
          } else {
            dealFunc(child);
            break;
          }
        }
      } else {
        dealFunc(node)
      }
    },
    /**
     * 使用jspdf默认分页，不认为计算分页高度
     */
    pageBreakByDefault(contentHeight) {
      const fragment = document.createDocumentFragment();
        this.addPage(fragment);
        const dealFunc = (node) => {
          const cloneChild = node.cloneNode(true);
          const height = node.offsetHeight;
          if (height === undefined) return;
          const domHeight = height + this.getMarginValue(node);
          // 当前页放置不下
          // contentHeight - pageBreakData.curHeight => 当前页剩余还能放置的高度
          // domHeight => 当前dom的实际高度
          // lessHeight => 如果为真 domHeight - (contentHeight - pageBreakData.curHeight) 的值为 下一页提前放置的高度
          // lessHeight => 如果为假 则代表当前页能放置此dom
          const lessHeight = (contentHeight - this.pageBreakData.curHeight) < domHeight;
          if (this.beforePageBreakClass(node) || lessHeight) {
            let complementHeight = 0;
            if (lessHeight) {
              complementHeight = -(contentHeight - this.pageBreakData.curHeight);
              // 将当前dom添加到当前页下
              this.pageBreakData.pageDiv.appendChild(node.cloneNode(true));
            }
            this.finishPage();
            this.addPage(fragment);
            if (lessHeight) {
              this.pageBreakData.curHeight = complementHeight;
            }
          }
          // 当前高度小于0，则代表本dom是继承自上一页的最后一个dom，所以设置marginTop为负值，向上偏移
          if (this.pageBreakData.curHeight < 0) {
            cloneChild.style.marginTop = this.pageBreakData.curHeight + 'px';
          }
          // 将当前dom添加到当前页下
          this.pageBreakData.pageDiv.appendChild(cloneChild);
          // 将计算高度累加起来
          this.pageBreakData.curHeight += domHeight;
          this.pageBreakData.totalHeight += domHeight;
        }
        const maxLen = this.$refs.default.children.length;
        for (let i = 0, len = maxLen; i < len; i++) {
          const item = this.$refs.default.children[i];
          dealFunc(item);
        }
        this.finishPage();
        this.generateStatus.totalPage = this.pageBreakData.pageNo;
        this.$refs.generate.appendChild(fragment);
    },
    /**
     * @description clone包含fragment的节点
     */
    _fragmentCloneNode(node) {
      let ele;
      // 检查是否是原生的fragment 或者 vue-frag 的 Fragment
      if (node instanceof DocumentFragment || (node.__vue__ && node.frag && node.frag.length)) {
        // 如果是原生 DocumentFragment 节点，创建一个新的 DocumentFragment 并复制子节点
        ele = document.createDocumentFragment();
        let child = node.firstChild;
        while (child) {
          if (child instanceof DocumentFragment || (child.__vue__ && child.frag && child.frag.length)) {
            // 递归处理子节点中的原生 DocumentFragment
            ele.appendChild(this._fragmentCloneNode(child));
          } else {
            const cloneChild = child.cloneNode(true);
            cloneChild.cloneOffset = {
              offsetHeight: child.offsetHeight
            }
            ele.appendChild(cloneChild);
          }
          child = child.nextSibling;
        }
      } else {
        // 否则，直接克隆节点
        ele = node.cloneNode(true);
      }
      return ele;
    },
    /**
     * 自动分页和默认分页前，判断是否有PageBreakClass，有则直接分页，再进行后续逻辑
     */
    beforePageBreakClass(ele) {
      const pageBreakClass = this.mergedPropsConfig.pageBreakClass;
      // 判断是否包含 分页标记类
      const hasPageBreak = ele.classList && ele.classList.contains(pageBreakClass);
      return hasPageBreak;
    },
    /**
     * 通过计算高度来进行自动分页
     */
    pageBreakByAuto(contentHeight) {
      const fragment = document.createDocumentFragment();
      this.addPage(fragment);
      const dealFunc = (node) => {
        const cloneChild = node.cloneNode(true);
        const height = node.offsetHeight;
        if (height === undefined) return;
        const domHeight = height + this.getMarginValue(node);
        // 当前剩余空间还能放下dom 那就在当前页dom下放置dom
        // 当前页剩余空间无法放置dom，新开一页dom
        if (this.beforePageBreakClass(node) || ((contentHeight - this.pageBreakData.curHeight) < domHeight)) {
          this.finishPage();
          this.addPage(fragment);
        }
        // 将当前dom添加到当前页下
        this.pageBreakData.pageDiv.appendChild(cloneChild);
        // 将计算高度累加起来
        this.pageBreakData.curHeight += domHeight;
        this.pageBreakData.totalHeight += domHeight;
      }
      const maxLen = this.$refs.default.children.length;
      for (let i = 0, len = maxLen; i < len; i++) {
        const item = this.$refs.default.children[i];
        dealFunc(item);
      }
      this.finishPage();
      this.generateStatus.totalPage = this.pageBreakData.pageNo;
      this.$refs.generate.appendChild(fragment);
    },
    /**
     * 获取dom元素的 上下边距之和
     * @param dom dom元素
     * @returns
     */
    getMarginValue(dom) {
      const domStyle = window.getComputedStyle(dom);
      return parseFloat(domStyle.marginTop) + parseFloat(domStyle.marginBottom);
    },
    /**
     * 单个分区生成pdf操作
     * @param item 节点dom
     * @returns
     */
    async asyncSingleAreaControl(i) {
      const canvas = await this.singleHandle(i)
      canvas && await this.areaPage(canvas, i)
    },
    /**
     * 单页pdf处理
    //  * @param root 总节点
     * @param index 分区索引
     */
    async singleHandle(i) {
      this.getDiv = document.getElementsByClassName(`page-no-${i + 1}`)[0];
      if (!this.getDiv) throw new Error('无法找到当前页dom');
      const res = await html2canvas(this.getDiv, {
        useCORS: true,
        allowTaint: true,
        scale: this.pdfInnerConfig.scale
      })
      return res;
    },
    calcCanvas(canvas) {
      const contentWidth = canvas.width;
      /**
       * html2canvas放大3.125倍时精度丢失导致多了2像素,分区里只有1页的情况
       * 3368: 高度285mm纸张html2canvas放大300dpi后像素
       * 3366：正常实际高度
       */
      let contentHeight = 0;
      if (canvas.height <= 3368) {
        contentHeight = this.pdfConfig.rectOriginPX300DPI.height;
      } else {
        contentHeight = canvas.height;
      }
      const pageHeight = Math.round(contentWidth / this.pdfConfig.rectOriginMM.numWidth * this.pdfConfig.rectOriginMM.numHeight);
      const imgWidth = this.pdfConfig.rectOriginMM.numWidth;
      const imgHeight = Math.ceil(this.pdfConfig.rectOriginMM.numWidth / contentWidth * contentHeight);
      return {
        contentHeight,
        pageHeight,
        imgWidth,
        imgHeight
      }
    },
    /**
     * 分割图片，并且添加到PDF内
     * @param positionData 相关数据
     * @param headerObj 页眉数据
     * @param footerObj 页脚数据
     */
    splitImgToAddPdf(positionData, headerObj, footerObj) {
      const {
        pageData,
        pageHeight
      } = positionData
      return new Promise((resolve) => {
        const img = new Image();
        img.src = pageData;
        img.onload = function() {
          this.generatePdfByImg(img, pageHeight, headerObj, footerObj, (status) => {
            if (status) resolve(status);
          })
        }
      })
    },
    /**
     * 图片生成pdf
     * @param img 图像数据
     * @param pageHeight 页面高度
     * @param splitDistance 分割长度 mm制
     * @param headerObj 页眉数据
     * @param footerObj 页脚数据
     */
    generatePdfByImg(img, pageHeight, headerObj, footerObj, callback) {
      let position = 0;
      const imgWidth = img.width;
      // 计算分割的次数
      const numSplits = this.pageBreakData.borderSegmentHeight.length;
      let imgSplitHeight = 0;
      // 依次添加分割后的图片到 PDF
      for (let i = 0; i < numSplits; i++) {
        const segment = this.pageBreakData.borderSegmentHeight[i] * this.pdfInnerConfig.scale;
        const startY = imgSplitHeight;
        imgSplitHeight += segment;
        // 创建一个临时的 canvas 用于裁剪图片
        const canvas = document.createElement('canvas');
        canvas.width = imgWidth;
        canvas.height = segment;
        const ctx = canvas.getContext('2d');
        ctx.drawImage(img, 0, startY, imgWidth, segment, 0, 0, imgWidth, segment);
        // 将裁剪后的图片转换为 Base64 数据
        const splitBase64 = canvas.toDataURL('image/jpeg', 1);
        // 页眉图片添加
        if (headerObj.headerImg) {
          this.PDF.addImage(headerObj.headerImg, 'JPEG', 0, position, headerObj.headerWidth, headerObj.headerHeight);
          position += headerObj.headerHeight;
        }
        const mmSplitHeight = Number(new Dpi().pxToMm(segment / this.pdfInnerConfig.scale));
        const mmImgWidth = Number(new Dpi().pxToMm(imgWidth / this.pdfInnerConfig.scale));
        // 添加分割后的图片到 PDF
        this.PDF.addImage(splitBase64, 'JPEG', 0, position, mmImgWidth, mmSplitHeight);
        position += mmSplitHeight;
        // 页脚图片添加
        if (footerObj.footerImg.length) {
          const mmPageHeight = Number(new Dpi().pxToMm(pageHeight / this.pdfInnerConfig.scale));
          position = mmPageHeight - footerObj.footerHeight;
          this.PDF.addImage(footerObj.footerImg[i], 'JPEG', 0, position, footerObj.footerWidth, footerObj.footerHeight);
          position += footerObj.footerHeight;
        }
        // 如果不是最后一次分割，添加新的页面
        if (i < numSplits - 1) {
          this.PDF.addPage();
          position = 0;
        }
      }
      callback(true);
    },
    /**
     * 分区pdf处理
     * @param canvas 各个分区dom转换后的canvas
     * @param areaNo 分区索引
     */
    areaPage(canvas, areaNo) {
      // 是否是第一个分区(作用于是否开始就addPage)
      const isFirstArea = areaNo === 0
      return new Promise((resolve) => {
        const {
          contentHeight,
          pageHeight,
          imgWidth,
          imgHeight
        } = this.calcCanvas(canvas)
        const pageData = canvas.toDataURL('image/jpeg', 1);
        const imgSupplementWidth = isBrower() ? 0.62 : 0.1;
        const imgSupplementHeight = isBrower() ? 0.32 : 1
        const positionData = {
          contentHeight,
          pageHeight,
          imgWidth,
          imgHeight,
          leftHeight: contentHeight,
          position: 0,
          pageData,
          imgSupplementWidth,
          imgSupplementHeight
        }
        // 显示当前已生成页码数
        // 非首个分区，得先addPage，因为不然会少一页 && 大于某个范围才新增一页，避免因为浮点数计算精度造成多增一页
        if (!isFirstArea && positionData.leftHeight > 0) {
          this.PDF.addPage()
        }
        this.autoGeneratePdf(positionData).then(res => {
          resolve(res);
        })
      })
    },
    /**
     * 自动模式下 生成pdf
     * @param positionData
     * @returns
     */
    autoGeneratePdf(positionData) {
      return new Promise((resolve) => {
        while (positionData.leftHeight > 0) {
          this.PDF.addImage(positionData.pageData, 'JPEG', 0, positionData.position, positionData.imgWidth + positionData.imgSupplementWidth, positionData.imgHeight + positionData.imgSupplementHeight);
          positionData.position -= this.pdfConfig.rectOriginMM.numHeight;
          positionData.leftHeight -= positionData.pageHeight
          // 大于某个范围才新增一页，避免因为浮点数计算精度造成多增一页
          if (positionData.leftHeight > 0) {
            this.PDF.addPage()
          }
        }
        resolve(true);
      })
    },
    /**
     * 预览PDF的遮罩层控制
     * @param status
     */
    maskVisibleControl(status = false) {
      this.maskVisible = status
    },
    /**
     * 组装生成返回结果
     * @param result 结果文件
     * @param callback 回调
     */
    decorateGenerateResult(result, callback) {
      const generateData = {
        ...this.generateStatus,
        result
      }
      callback(generateData);
    },
    /**
     * 生成pdf
     */
    async generatePdf(callback, {
      fileName = 'GeneratePdf插件pdf插件',
      output = 'file'
    } = {}) {
      this.mode = 'generate';
      const status = await this.splitAndGenerateDom();
      let result = null;
      this.decorateGenerateResult(result, callback);
      if (!status) return;
      this.generateStatus.status = 'process';
      this.PDF = new JsPDF(this.mergedPropsConfig.direction === 'Vertical' ? 'p' : 'l', 'mm', [this.pdfConfig.rectOriginMM.numWidth, this.pdfConfig.rectOriginMM.numHeight]); // pdf实例
      const childs = this.$refs.generate.querySelectorAll('.page-no');
      for (let i = 0, len = childs.length; i < len; i++) {
        await this.asyncSingleAreaControl(i);
        this.generateStatus.process = Number(((i + 1) / this.generateStatus.totalPage).toFixed(2)) * 100;
        this.generateStatus.curPage = i + 1;
        this.decorateGenerateResult(result, callback);
      }
      result = this.PDF.output('blob');
      if (output === 'file') {
        result = blobUriToFile(result, fileName);
      }
      // 是否在生成pdf完成后自动下载到浏览器内
      if (this.mergedPropsConfig.autoDownload) {
        this.PDF.save(fileName);
      }
      this.generateStatus.status = 'finish';
      this.decorateGenerateResult(result, callback);
    },
    /**
     * 预览pdf
     */
    async previewPdf() {
      this.mode = 'preview';
      const status = await this.splitAndGenerateDom();
      if (!status) return;
      this.maskVisibleControl(true);
    }
  }
}
</script>

<style lang="scss" src="./index.scss"></style>
